<?php

/**
 * Restful Objects Spec 2.2.1: Summary
 * The following table summarizes the links (to other representations) that
 * may be present in the object representation.
 *                                                              update
 *                           property collection action persist property delete
 *                           -------- ---------- ------ ------- -------- ------
 * Persistent Entity              yes        yes    yes     ---      yes    yes
 * Proto-persistent Entity  (in-lined)(in-lined)    ---     yes      ---    ---
 * View model               (in-lined)(in-lined)    ---     ---      ---    ---
 * Addressable View model   (in-lined)(in-lined)    yes     ---      ---    ---
 * Domain Service                 ---        ---    yes     ---      ---    ---
 *
 * ======  ========  =========  ============  ===========  =========  ===========
 * OBJECTS
 * ======  ========  =========  ============  ===========  =========  ===========
 *         Objects      Object        Object       Object     Object       Object
 *         Of Type                  Property   Collection     Action       Action
 *                                                                         Invoke
 * ======  ========  =========  ============  ===========  =========  ===========
 *          object/    object/       object/      object/    object/      object/
 *          {Table}   {Table}/      {Table}/     {Table}/   {Table}/     {Table}/
 *                       {Key}        {Key}/       {Key}/     {Key}/       {Key}/
 *                                 property/  collection/    action/      action/
 *                                {Property} {Collection}   {Action}    {Action}/
 *                                                                         invoke
 * ------  --------  ---------  ------------  -----------  ---------  -----------
 * GET         n/a      object      property   collection     action       invoke
 *             405    summary,       details      details     prompt      (action
 *                      member     and value  and content             known to be
 *                    summary,                                        query-only)
 *                    property
 *                      values
 * ------  --------  ---------  ------------  -----------  ---------  -----------
 * PUT         n/a   update or     update or   add object    n/a 405       invoke
 *             405       clear   clear value      (if Set                 (action
 *                    multiple                 semantics)             known to be
 *                    property                                        idempotent)
 *                      values
 * ------  --------  ---------  ------------  -----------  ---------  -----------
 * DELETE       n/a     delete   clear value       remove    n/a 405      n/a 405
 *              405     object                     object
 * ------  --------  ---------  ------------  -----------  ---------  -----------
 * POST     persist    n/a 405       n/a 405   add object    n/a 405       invoke
 *         instance                              (if List             (action not
 *                                             semantics)             known to be
 *                                                                    idempotent)
 * ------  --------  ---------  ------------  -----------  ---------  -----------
 *
 * ======== ==================  =========================  ======================
 * SERVICES
 * ======== ==================  =========================  ======================
 *                     Service             Service Action   Service Action Invoke
 * ------  -------------------  -------------------------  ----------------------
 *         service/{ServiceFunction}       service/{ServiceFunction}/    service/{ServiceFunction}/
 *                                        action/{Action}         action/{Action}
 *                                                                        /invoke
 * ------  -------------------  -------------------------  ----------------------
 * GET                 service              action prompt    invoke (action known
 *                    summary,                                  to be query-only)
 *                      action
 *                     summary
 * ------  -------------------  -------------------------  ----------------------
 * PUT                 n/a 405                    n/a 405    invoke (action known
 *                                                              to be idempotent)
 * ------  -------------------  -------------------------  ----------------------
 * DELETE              n/a 405                    n/a 405                 n/a 405
 * ------  -------------------  -------------------------  ----------------------
 * POST                n/a 405                    n/a 405      invoke (action not
 *                                                        known to be idempotent)
 * ------  -------------------  -------------------------  ----------------------
 *
 * */

class FrontAPIController extends SimpleRestController {

    # the parent controller class went through the trouble of calling the specific method,
    # but most of our logic is in common between these, so we just call execute().
    public function get()    { $this->execute(); }
    public function post()   { $this->execute(); }
    public function put()    { $this->execute(); }
    public function delete() { $this->execute(); }

    private function execute() {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $response             = array();
        $matches              = array();
        $http_method          = $this->request['method'];
        $resource             = $this->request['resource'];
        $resource_parts       = explode('/', $resource);
        $num_parts            = count($resource_parts);
        $request_data         = isset($this->request['params']) ? $this->request['params'] : array();
        if ($num_parts == 0) {
            throw new Exception('Method Not Allowed', 405); # this should never happen. no PATH_INFO gets interpreted as the 'index' string.
        }
        # /                                             : API Directory (to objects and services)
        # /index                                        : API Directory (to objects and services)
        elseif ($resource_parts[0] === 'index' || $resource_parts[0] === '') {
            $this->execute_api_index($response);
        }
        # /object/_phys/{DB}.{Schema}/{Table}                : Objects of Type
        # /object/_phys/{DB}.{Schema}/{Table}/_meta          : Objects of Type (metadata)
        # /object/_phys/{DB}.{Schema}/{Table}/{Key}          : Object

        # /object/{DB}/{Table}                               : Objects of Type
        # /object/{DB}/{Table}/_meta                         : Objects of Type (metadata)
        # /object/{DB}/{Table}/{Key}                         : Object

        # /object/{DB}/{Table}/{Key}/property/{Property}     : Object Property
        # /object/{DB}/{Table}/{Key}/collection/{Collection} : Object Collection
        # /object/{DB}/{Table}/{Key}/action/{Action}         : Object Action
        elseif ($resource_parts[0] === 'object') {
            $object_set = new API\ObjectSet();
            if ($num_parts == 1) {
                $this->execute_object_index($response);
            }
            else {
                # Note: It is a privileged operation to change the app_env (TODO)
                $db_env = $options['app_env'];   # i.e. we assume we use the default setting
                if ($num_parts >= 3 && preg_match('/^_([a-zA-Z][a-zA-Z0-9_-]*)$/', $resource_parts[1], $matches)) {
                    $db_env = $matches[1];
                    array_splice($resource_parts, 1, 1);  # remove the db_env element
                    $num_parts--;
                }

                if ($num_parts >= 2) {
                    # /object/_phys/{DB}.{Schema}                    : Physical Schema Directory (list of tables)
                    # /object/{DB}                                   : Logical Database Directory (list of tables)
                    if ($num_parts == 2) {
                        $db_name        = $resource_parts[1];
                        $db             = $context->database($db_name, array('app_env' => $db_env));
                        throw new Exception('Method Not Allowed', 405);
                    }
                    else {
                        $table_name    = $resource_parts[2];
                        $db_name        = ($resource_parts[1] === 'global') ? $this->get_database_name($table_name) : $resource_parts[1];
                        $db             = $context->database($db_name, array('app_env' => $db_env));
                        if ($num_parts == 3) {
                            $object_set->execute_object_db_table($http_method, $request_data, $response, $db, $table_name);
                        }
                        elseif ($num_parts == 4) {
                            if ($resource_parts[3] === '_meta') {
                                $object_set->execute_object_db_table_meta($http_method, $request_data, $response, $db, $table_name);
                            }
                            elseif ($resource_parts[3] === '_dbmeta') {
                                $object_set->execute_object_db_table_dbmeta($http_method, $request_data, $response, $db, $table_name);
                            }
                            elseif ($resource_parts[3] === '_doc') {
                                $this->execute_object_db_table_doc($http_method, $request_data, $response, $db, $table_name);
                            }
                            else {
                                $key = $resource_parts[3];
                                $object_set->execute_object_db_table_key($http_method, $request_data, $response, $db, $table_name, $key);
                            }
                        }
                        elseif ($num_parts == 5) {
                            throw new Exception('Method Not Allowed', 405); # BAD: /objects/{Table}/{Key}/whatever is illegal
                        }
                        elseif ($resource_parts[4] === 'property') {
                            if ($num_parts == 6) {
                                throw new Exception('Method Not Allowed', 405); # TODO: /objects/{Table}/{Key}/property/{Property}  Object Property
                            }
                            else {
                                throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with it
                            }
                        }
                        elseif ($resource_parts[4] === 'collection') {
                            if ($num_parts == 6) {
                                throw new Exception('Method Not Allowed', 405); # TODO: /objects/{Table}/{Key}/collection/{Collection}  Object Collection
                            }
                            else {
                                throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with it
                            }
                        }
                        elseif ($resource_parts[4] === 'action') {   # the spec is ambiguous on which it should be
                            if ($num_parts == 6) {
                                throw new Exception('Method Not Allowed', 405); # TODO: /objects/{Table}/{Key}/action/{Action}  Object Action
                            }
                            elseif ($num_parts == 7 && $resource_parts[6] === 'invoke') {
                                throw new Exception('Method Not Allowed', 405); # TODO: /objects/{Table}/{Key}/action/{Action}/invoke  Object Action invoke
                            }
                            else {
                                throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with it
                            }
                        }
                        else {
                            throw new Exception('Method Not Allowed', 405); # BAD
                        }
                    }
                }
            }
        }
        # service/{ServicePackage}/{ServiceFunction}                          : Service
        # service/{ServicePackage}/{ServiceFunction}/action/{Action}          : [TBD] Service Action
        # service/{ServicePackage}/{ServiceFunction}/action/{Action}/invoke   : [TBD] Service Action Invoke
        elseif ($resource_parts[0] === 'service') {
            if ($num_parts == 1) {
                $this->execute_service_index($http_method, $request_data, $response);
            }
            elseif (preg_match('/^[A-Z][A-Za-z0-9_]*$/', $resource_parts[1])) {
                $num = count($resource_parts);
                $class = $resource_parts[1];
                if (preg_match('/^[a-z][A-Za-z0-9_]*$/', $resource_parts[$num-1])) {  # method
                    $method = $resource_parts[$num-1];
                    for ($i = 2; $i < $num - 1; $i++) {
                        if (!preg_match('/^[A-Z][A-Za-z0-9_]*$/', $resource_parts[$i])) throw new Exception('Method Not Allowed', 405);
                        $class = $class . '\\' . $resource_parts[$i];
                    }
                    $this->execute_service_class_action($http_method, $request_data, $response, $class, $method);
                }
                else {
                    for ($i = 2; $i < $num; $i++) {
                        if (!preg_match('/^[A-Z][A-Za-z0-9_]*$/', $resource_parts[$i])) throw new Exception('Method Not Allowed', 405);
                        $class = $class . '\\' . $resource_parts[$i];
                    }
                    $this->execute_service_class_index($http_method, $request_data, $response, $class);
                }
            }
            elseif (preg_match('/^[a-z][A-Za-z0-9_]*$/', $resource_parts[1])) {
                $num = count($resource_parts);
                $method = "";
                $arg = null;
                if ($resource_parts[1] === 'context') {
                    if ($num > 3) throw new Exception('Method Not Allowed', 405);
                    elseif ($num == 3) {
                        $object = $context;
                        $method = $resource_parts[2];
                    }
                    elseif ($num == 2) {
                        $object = $context;
                    }
                }
                elseif ($resource_parts[1] === 'authentication') {
                    if ($num > 4) throw new Exception('Method Not Allowed', 405);
                    elseif ($num == 4) {
                        $name   = $resource_parts[2];
                        $object = $context->authentication($name);
                        if ($resource_parts[3] !== 'index') $method = $resource_parts[3];
                    }
                    elseif ($num == 3) {
                        $name   = $resource_parts[2];
                        if (preg_match('/^[a-z]+$/', $name)) {
                            $object = $context->authentication($name);
                        }
                        else {
                            $method = $name;
                            $name   = 'default';
                            $object = $context->authentication();
                        }
                    }
                    elseif ($num == 2) {
                        $name   = 'default';
                        $object = $context->authentication();
                    }
                }
                else {
                    if ($num > 4) throw new Exception('Method Not Allowed', 405);
                    elseif ($num >= 3) {
                        $name   = $resource_parts[1];
                        $class  = 'API\\' . ucfirst($name);
                        $object = $context->sessionObject($name, array('class' => $class));
                        if ($resource_parts[2] !== 'index') $method = $resource_parts[2];
                        if ($num == 4) $arg = $resource_parts[3];
                    }
                    elseif ($num == 2) {
                        $name   = $resource_parts[1];
                        $class  = 'API\\' . ucfirst($name);
                        $object = $context->sessionObject($name, array('class' => $class));
                    }
                }
                if ($method) {
                    $this->execute_service_pkg_method($http_method, $request_data, $response, $object, $method, $arg);
                }
                else {
                    if (is_a($object,'App\\Service')) {
                        $response['service_type'] = $object->type;
                        $response['name']         = $object->name;
                    }
                    $response['class'] = get_class($object);
                    $this->execute_service_pkg($http_method, $request_data, $response, $object);
                }
            }
            elseif ($num_parts == 2) {
                throw new Exception('Method Not Allowed', 405); # TODO: Implement /services/{ServiceFunction} (Service)
            }
            elseif ($num_parts == 3) {
                throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with /services/{ServiceFunction}/action. List all {Actions}?
            }
            elseif ($resource_parts[2] === 'action') {
                if ($num_parts == 4) {
                    throw new Exception('Method Not Allowed', 405); # TODO: /services/{ServiceFunction}/action/{Action}  Service Action
                }
                elseif ($num_parts == 5 && $resource_parts[4] === 'invoke') {
                    throw new Exception('Method Not Allowed', 405); # TODO: /services/{ServiceFunction}/action/{Action}/invoke  Service Action invoke
                }
                else {
                    throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with it
                }
            }
            else {
                throw new Exception('Method Not Allowed', 405); # BAD
            }
        }
        else {
            throw new Exception('Method Not Allowed', 405); # BAD: not sure what to do with it
        }
        if (isset($response['_meta'])) {
            $response['_meta']['http_method'] = $http_method;
            $response['_meta']['resource'] = $resource;
        }
        $this->response = $response;
        $this->responseStatus = 200;
        if ($trace) trace_exit();
    }

    protected function execute_api_index (&$response) {
        global $trace;
        if ($trace) trace_entry();
        $http_method = $this->request['method'];
        $server      = $this->request['server'];
        $script_name = $this->request['script_name'];
        $resource    = $this->request['resource'];
        if ($http_method === 'get') {
            $response['_meta']  = array(
                'doc' => "This is the root (index) of the API tree. Follow links to access the objects and services."
            );
            $response['_links'] = array(
                'self'    => array('href' => "$server$script_name/$resource"),
                'object'  => array('href' => "$server$script_name/object"),
                'service' => array('href' => "$server$script_name/service")
            );
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    protected function execute_object_index (&$response) {
        global $trace;
        if ($trace) trace_entry();
        $http_method = $this->request['method'];
        $server      = $this->request['server'];
        $script_name = $this->request['script_name'];
        $resource    = $this->request['resource'];
        if ($http_method === 'get') {
            $response['_meta']  = array(
                'doc' => "This is the root (index) of the object tree. Follow links to access the various object types."
            );
            $response['_links'] = array(
                'self' => array('href' => "$server$script_name/$resource"),
                'parent' => array('href' => "$server$script_name/index")
            );
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    public function execute_object_db_table_doc ($http_method, $request, &$response, $db, $table) {
        global $context, $trace;
        if ($trace) trace_entry();
        #$get_options = array();
        #$is_aggregate_query = 0;
        #if ((isset($request['aggregate']) && $request['aggregate'] && $request['aggregate'] !== 'false') || isset($request['group_columns'])) {
        #    $is_aggregate_query = 1;
        #    $get_options['aggregate'] = $is_aggregate_query;
        #}
        #if (isset($request['view']))    { $get_options['view'] = $request['view']; }
        #if (isset($request['columns'])) { $get_options['columns'] = explode(',',$request['columns']); }
        #$table_def        = $db->get_table_def($table, null, $get_options);
        #$response['data'] = $table_def;
        #if ($http_method !== 'get') { throw new Exception('Method Not Allowed', 405); }
        #$response['success'] = true;

        $object_set = new API\ObjectSet();
        $this->get_documentation($object_set, $response);

        if ($trace) trace_exit();
    }

    protected function execute_service_index ($http_method, $request, &$response) {
        global $context, $trace;
        if ($trace) trace_entry();
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    protected function execute_service_class_index ($http_method, $request, &$response, $class) {
        global $context, $trace;
        if ($trace) trace_entry();
        $response['success'] = true;
        if ($http_method === 'get') {
            $this->get_documentation($class, $response, 2);
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        if ($trace) trace_exit($response);
    }

    /** execute_service_class_action($request, &$response, $class, $method)
      * @param request an associative array of input parameters
      * @param request an associative array of outputs to be returned to the caller (note: the return value of the called method goes in the 'return' slot)
      */
    protected function execute_service_class_action ($http_method, $request, &$response, $class, $method) {
        global $context, $trace;
        if ($trace) trace_entry();
        $service = $context->sessionObject('anonymous', array('class' => $class));
        $return  = $service->$method($request, $response);
        $response['success'] = true;
        if (isset($return)) $response['return'] = $return;
        if ($trace) trace_exit($response);
    }

    protected function execute_service_pkg ($http_method, $request, &$response, $object) {
        global $context, $trace;
        if ($trace) trace_entry();
        $response['success'] = true;
        if ($http_method === 'get') {
            $this->get_documentation($object, $response);
        }
        else {
            throw new Exception('Method Not Allowed', 405);
        }
        if ($trace) trace_exit($response);
    }

    private function get_documentation ($object, &$response, $type = 1) {
        global $context, $trace;
        if ($trace) trace_entry();
        if ($type == 1) {
            $reflect       = new ReflectionObject($object);
            $methods_array = array();
            foreach ($reflect->getMethods() as $reflectmethod) {
                $method_name  = $reflectmethod->name;
                if ($method_name[0] != '_') {
                    $methods_array[$method_name] = array();
                    $doc = $this->uncomment_doc($reflectmethod->getDocComment());
                    if ($doc) $methods_array[$method_name]['doc'] = explode("\n", $doc);
                    $phys_params_array = $reflectmethod->getParameters();
                    $doc = $this->uncomment_doc($reflectmethod->getDocComment());
                    if (count($phys_params_array) == 2 && $phys_params_array[0]->name === 'request' && $phys_params_array[1]->name === 'response') {
                        $params_array = array();
                        $return_params_array = array();
                        $doc_array = array();
                        $doclines = explode("\n",$doc);
                        foreach ($doclines as $docline) {
                            if (preg_match('/^@param +([a-z]+) +([a-zA-Z_]+) +\\[OUT\\]/', $docline, $matches)) {
                                $return_params_array[] = $docline;
                            }
                            elseif (preg_match('/^@param +([a-z]+) +([a-zA-Z_]+)/', $docline, $matches)) {
                                $params_array[] = $docline;
                            }
                            else {
                                $doc_array[] = $docline;
                            }
                        }
                        $methods_array[$method_name] = array(
                            'doc' => $doc_array,
                            'params' => $params_array,
                            'return_params' => $return_params_array
                        );
                    }
                    else {
                        $params_array = array();
                        foreach ($phys_params_array as $num => $param) {
                            $params_array[$num] = array(
                                'name'   => $param->name
                            );
                        }
                        $methods_array[$method_name] = array(
                            #'phys_params' => $phys_params_array,
                            'params' => $params_array
                        );
                        if ($doc) $methods_array[$method_name]['doc'] = explode("\n", $doc);
                    }
                    $methods_array[$method_name]['params'] = $params_array;
                }
            }
            $classdoc = $this->uncomment_doc($reflect->getDocComment());
            if ($classdoc) $response['doc'] = $classdoc;
            $response['methods'] = $methods_array;
        }
        else {
            $reflect       = new ReflectionClass($class);
            $methods_array = array();
            foreach ($reflect->getMethods() as $reflectmethod) {
                $method_name  = $reflectmethod->name;
                $params_array = array();
                $phys_params_array = $reflectmethod->getParameters();
                $doc = $this->uncomment_doc($reflectmethod->getDocComment());
                if (count($phys_params_array) == 2 && $phys_params_array[0]->name === 'request' && $phys_params_array[1]->name === 'response') {
                }
                else {
                    foreach ($phys_params_array as $num => $param) {
                        $params_array[$num] = array(
                            'name'   => $param->name
                        );
                    }
                    $methods_array[$method_name] = array(
                        'phys_params' => $phys_params_array,
                        'params' => $params_array
                    );
                    if ($doc) $methods_array[$method_name]['doc'] = $doc;
                }
            }
            $response['methods'] = $methods_array;
        }
        if ($trace) trace_exit($response);
    }

    private function uncomment_doc ($doc_comment) {
        global $trace;
        if ($trace) trace_entry();
        if ($doc_comment) {
            $doc = substr($doc_comment, 3, -2);
            $doc = ltrim(rtrim(preg_replace('/^ *\\* ?/m','',$doc)));
        }
        else {
            $doc = $doc_comment;
        }
        if ($trace) trace_exit($doc);
        return($doc);
    }

    protected function execute_service_pkg_method ($http_method, $request, &$response, $object, $method, $arg = null) {
        global $context, $trace;
        if ($trace) trace_entry();
        $request['_http_method'] = $http_method;

        if (!method_exists($object, $method)) {
            throw new Exception('Method Not Allowed', 405); # this should never happen. no PATH_INFO gets interpreted as the 'index' string.
        }

        if (isset($arg)) {
            $return = $object->$method($request, $response, $arg);
        }
        else {
            $return = $object->$method($request, $response);
        }
        if (isset($return)) $response['return'] = $return;

        if ($trace) trace_exit($response);
    }

    protected function get_database_name ($table_name) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $db_name = (isset($options["$table_name.database"])) ? $options["$table_name.database"] : 'auth';
        if ($trace) trace_exit($db_name);
        return($db_name);
    }
}
